﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Reflection;

namespace CommonT4
{
	public static class HelperExtensionT4
	{
		public static IEnumerable<Tuple<Type, Type>> GetConvertTypeMappings(this BaseTemplate template, Assembly srcA, Assembly destA)
		{

			var matchingTypes = from s in srcA.GetTypes()
								join d in destA.GetTypes()
									on s.Name equals d.Name//intersect sets on Type.Name
								let isEnumerable = s.GetInterface("IEnumerable") != null || d.GetInterface("IEnumerable") != null
								//let includes = template.MustIncludeSrcTypeFullNames == null || template.MustIncludeSrcTypeFullNames.Contains(s.Name)
								let excludes = template.ExcludeTypeFullNameSubstrings == null || !template.ExcludeTypeFullNameSubstrings.Any(substr => s.Name.Contains(substr))
								where !isEnumerable
								&& excludes //&& includes
								&& s.Inherits(template)
								&& !s.IsAbstract
								&& !s.IsGenericTypeDefinition
								select new Tuple<Type, Type>(s, d);
			return matchingTypes.ToArray();
		}

		/// <summary>
		/// returns: Tuple (dest property Type,src property Type, property name)
		/// </summary>
		/// <param name="template"></param>
		/// <param name="destType"></param>
		/// <param name="srcType"></param>
		/// <returns>Tuple (dest property Type,src property Type, property name)</returns>
		public static IEnumerable<Tuple<Type, Type, string>> GetCommonConvertProperties(this BaseTemplate template, Type destType, Type srcType)
		{
			var commonProperties = from d in destType.GetProperties()
								   join s in srcType.GetProperties()
										on d.Name equals s.Name

								   //Possibly duplicate code from GetConvertTypeMappings? but is not necessarily the same.
								   let isRecursivelyDefined = d.PropertyType == destType || s.PropertyType == srcType
								   //let includes = template.MustIncludeSrcTypeFullNames == null || template.MustIncludeSrcTypeFullNames.Contains(s.Name)
								   let excludes = template.ExcludeTypeFullNameSubstrings == null || !template.ExcludeTypeFullNameSubstrings.Any(substr => s.Name.Contains(substr))
								   where !isRecursivelyDefined
									   //&& s.PropertyType.Inherits(template) //NO. does not need to inherit from a user-specified Type if it's a property.
								   && excludes//&& includes 
								   && s.CanRead && d.CanRead
								   && s.CanWrite && d.CanWrite
								   select new Tuple<Type, Type, string>(d.PropertyType, s.PropertyType, s.Name);
			//select new { desttype = destP.PropertyType, srctype = srcP.PropertyType, Name = srcP.Name };
			return commonProperties;
		}

		/// <summary>
		/// true if the Type inherits from any of the specified types in BaseTemplate.MustInheritFromSrcTypes
		/// </summary>
		/// <param name="src">source (unserializable) Assembly's Type</param>
		/// <param name="template">true if the Type inherits from any of the specified types in BaseTemplate.MustInheritFromSrcTypes</param>
		/// <returns></returns>
		static bool Inherits(this Type src, BaseTemplate template)
		{
			if (template.MustInheritFromSrcTypes == null || template.MustInheritFromSrcTypes.Count() == 0)
				return true;

			Assembly srcA = src.Assembly;
			foreach (string fullname in template.MustInheritFromSrcTypes)
				if (src.IsSubclassOf(srcA.GetType(fullname)))
					return true;

			return false;
		}

		public static string DynamicVariableNameDeclarationsString(int start, int end)
		{
			string declarations = "";
			for (int i = start; i <= end; i++)
			{
				declarations += string.Format("src{0}, dest{0},", i);
			}
			declarations = declarations.TrimEnd(',') + ";";
			return declarations;
		}

		static bool IsNullableType(this Type t)
		{
			return t.IsValueType && t.Name == "Nullable`1";
		}


		/// <summary>
		/// callee has control of stack?
		/// </summary>
		/// <param name="template"></param>
		/// <param name="destType"></param>
		/// <param name="srcType"></param>
		/// <param name="lvl"></param>
		public static void WriteConvertType(BaseTemplate template, Type destType, Type srcType, Stack<Type> stack = null)//, int lvl = 0)
		{
			if (stack == null)
				stack = new Stack<Type>();

			if (srcType == destType) //typeof(System.String))
			{
				template.WriteLine("dest{0} = src{0};", stack.Count);
				return;
			}
			else if (srcType.IsNullableType())
			{
				template.WriteLine("dest{0} = new {1}(default({2}));", stack.Count, destType.ToGenericTypeFullNameString(), destType.GetGenericArguments()[0].ToGenericTypeFullNameString());
				//assume it's a Nullable<ValueType>				
				return;//do not attempt to set Value, HasValue.
			}
			else
				template.WriteLine("dest{0} = new {1}();", stack.Count, destType.ToGenericTypeFullNameString());

			var commonProperties = from tuple in template.GetCommonConvertProperties(destType, srcType)
								   select new { desttype = tuple.Item1, srctype = tuple.Item2, Name = tuple.Item3 };
			foreach (var common in commonProperties)
			{
				Type srcElementType;
				Type mappedType;
				if (common.desttype == common.srctype || template.IsTypeMapped(common.desttype, out mappedType))//base case: would-be return;
					template.WriteLine("dest{0}.{1} = src{2}.{3};", stack.Count, common.Name, stack.Count, common.Name);
				else if (common.srctype.isGenericEnumerableType(out srcElementType))
				{
					//get matching generic argument/element Type from dest. Assembly or mscorlib:
					Type destElementType = (from dtype in template.DestAssembly.GetTypes().Union(mscorlib.GetTypes())
											where dtype.Name == srcElementType.Name
											select dtype).First();
					template.WriteLine("if (src{0}.{1} != null) {{", stack.Count, common.Name);
					template.PushIndent("    ");
					template.WriteLine("dest{0}.{1} = new {2}();"
						, stack.Count
						, common.Name
						, common.desttype.ToGenericTypeFullNameString());
					//caller checks stack:
					if (!stack.Contains(common.desttype))
					{
						template.WriteLine("for (int i{0} = 0; i{0} < ((IList<{2}>)src{0}.{1}).Count; i{0}++)"
						   , stack.Count
						   , common.Name
						   , srcElementType.ToGenericTypeFullNameString()
						   );
						template.WriteLine("{");
						template.PushIndent("    ");
						template.WriteLine("src{0} = src{1}.{2}[i{1}];", stack.Count + 1, stack.Count, common.Name);
						stack.Push(destElementType);
						WriteConvertType(template, destElementType, srcElementType, stack);//, lvl + 1);
						stack.Pop();
						template.WriteLine("dest{0}.{1}.Add(dest{2});", stack.Count, common.Name, stack.Count + 1);
						template.PopIndent();
						template.WriteLine("}");//end for
					}
					template.PopIndent();
					template.WriteLine("}");//end if
				}
				else
				{
					if (!stack.Contains(common.desttype))
					{
						template.WriteLine("src{0} = src{1}.{2};", stack.Count + 1, stack.Count, common.Name);//if property type not equal (and not IEnumerable)
						template.WriteLine("if (src{0} is {1}) {{", stack.Count + 1, common.srctype.ToGenericTypeFullNameString());//if src != null
						template.PushIndent("    ");//CurrentIndent += 1;
						stack.Push(common.desttype);
						WriteConvertType(template, common.desttype, common.srctype, stack);// lvl + 1);
						stack.Pop();
						template.WriteLine("dest{0}.{1} = dest{2};", stack.Count, common.Name, stack.Count + 1);//finally, set the property upon return
						template.PopIndent();
						template.WriteLine("}");
					}
				}
			}
			return;
		}


		//static Stack<Type> stack = new Stack<Type>();


		public static IEnumerable<Tuple<string, int>> GetEnumTuples(Type enumType)
		{
			List<Tuple<string, int>> list = new List<Tuple<string, int>>();
			foreach (string name in enumType.GetEnumNames())
			{
				int value = (int)Enum.Parse(enumType, name);
				list.Add(new Tuple<string, int>(name, value));
			}
			return list;
		}


		/// <summary>
		/// returns re-mapped Type.FullName if found in TypeMappings, otherwise returns original Type.FullName.
		/// </summary>
		/// <param name="template"></param>
		/// <param name="src"></param>
		/// <returns></returns>
		public static string RemapTypeFullName(this BaseTemplate template, string src)
		{
			if (template.TypeMappings != null)
			{
				foreach (var kvp in template.TypeMappings)
				{
					while (src.Contains(kvp.Key))
					{
						src = src.Replace(kvp.Key, kvp.Value);
					}
				}
			}
			return src;
		}

		public static string RemapNamespace(this BaseTemplate template, string src)
		{
			if (template.NamespaceMappings != null)
			{
				foreach (var kvp in template.NamespaceMappings)
				{
					while (src.Contains(kvp.Key))
					{
						src = src.Replace(kvp.Key, kvp.Value);
					}
				}
			}
			return src;
		}

		static bool IsTypeMapped(this BaseTemplate template, Type fromType, out Type toType)
		{
			string remappedFullName = RemapTypeFullName(template, fromType.FullName);
			if (remappedFullName == fromType.FullName)
			{
				toType = fromType;
				return false;
			}
			toType = template.DestAssembly.GetType(remappedFullName);
			return true;

		}

		/// <summary>
		/// "action" "if" "condition" : code review?
		/// </summary>
		/// <param name="propertyType"></param>
		/// <returns></returns>
		static Type RemapIfIEnumerable(Type propertyType)
		{
			Type elementType;
			if (propertyType.isGenericEnumerableType(out elementType))
			{
				var listInstance = Activator.CreateInstance(typeof(List<>).MakeGenericType(elementType));
				return listInstance.GetType();
			}
			else
				return propertyType;

		}

		/// <summary>
		/// warning: excludes indexer properties (which tend to act more like methods than data anyway).
		/// </summary>		
		public static IEnumerable<Tuple<string, string>> GetPropertiesToSerialize(this BaseTemplate template, Type srcType)
		{

			var properties = from p in srcType.GetProperties()
							 let PropertyType = RemapIfIEnumerable(p.PropertyType)
							 let fullName = RemapTypeFullName(template, PropertyType.ToGenericTypeFullNameString())
							 where p.CanRead && p.CanWrite
							 && p.PropertyType != null
							 && p.PropertyType.FullName != null
							&& !ContainsExcludedSubstr(p.PropertyType.FullName, template.ExcludeTypeFullNameSubstrings)
							&& !p.PropertyType.IsInterface
							&& !p.PropertyType.IsGenericTypeDefinition
							&& !(p.Name == "Item" && p.GetIndexParameters().Count() > 0)//exclude indexers: we're serializing data not methods.
							 select new Tuple<string, string>(fullName, p.Name);

			return properties;
		}

		public static IEnumerable<Type> GetTypesToSerialize(this Assembly src, IEnumerable<string> fullname_substr_excludes)
		{
			var types = from t in src.GetTypes()
						where !ContainsExcludedSubstr(t.FullName, fullname_substr_excludes)
							//&& !t.FullName.Contains("DisplayClass")							
						&& !t.IsNested
						&& !t.IsNotPublic
						&& !t.IsInterface	//we don't need interfaces since we're not doing inheritance or serializing interfaces properties.
						//&& !t.IsGenericTypeDefinition
						orderby t.Namespace
						select t;
			var list = types.ToList();
			return list;
		}

		/// <summary>
		/// naturally we also exclude any property of Type which we're not going to serialize.
		/// </summary>		
		static bool ContainsExcludedSubstr(string str, IEnumerable<string> substr_excludes)
		{
			if (substr_excludes == null)
				return false;

			foreach (var exclude in substr_excludes)
				if (str.Contains(exclude))
					return true;

			return false;
		}


		/// <summary>
		/// returns same Type as caller's if none found.
		/// </summary>
		/// <param name="t"></param>
		/// <returns></returns>
		public static Type BasemostType(Type t)
		{
			Type b = t;
			while (!(b.BaseType == null || b.BaseType == typeof(System.Object)
				|| b.BaseType == typeof(System.ValueType)) || b.BaseType == typeof(System.Enum))
			{
				b = b.BaseType;
			}
			return b;
		}

		public static string ToGenericTypeFullNameString(this Type t)
		{
			if (t.FullName == null && t.IsGenericParameter)
				return t.GenericParameterPosition == 0 ? "T" : "T" + t.GenericParameterPosition;

			if (!t.IsGenericType)
				return t.FullName;

			string value = t.FullName.Substring(0, t.FullName.IndexOf('`')) + "<";
			Type[] genericArgs = t.GetGenericArguments();
			List<string> list = new List<string>();
			for (int i = 0; i < genericArgs.Length; i++)
			{
				value += "{" + i + "},";
				string s = ToGenericTypeFullNameString(genericArgs[i]);
				list.Add(s);
			}
			value = value.TrimEnd(',');
			value += ">";
			value = string.Format(value, list.ToArray<string>());
			return value;

		}

		public static string ToGenericTypeNameString(this Type t)
		{
			string fullname = ToGenericTypeFullNameString(t);
			fullname = fullname.Substring(fullname.LastIndexOf('.') + 1);
			return fullname;
		}


		public static bool isGenericEnumerableType(this Type enumerableType, out Type elementType)
		{
			if (enumerableType.GetInterfaces().Where(i => i.Name == "IEnumerable`1").Count() > 0
				&& enumerableType != typeof(System.String))
			{
				//this assumes all interfaces on this generic type use the same generic argument (0):
				Type genericcollectionType = enumerableType.GetInterfaces().First(i => i.IsGenericType);
				elementType = genericcollectionType.GetGenericArguments()[0];
				return true;
			}
			else
			{
				elementType = null;
				return false;
			}

		}
		public static bool isEnumerableType(Type t)
		{
			return t.GetInterfaces().Contains(typeof(System.Collections.IEnumerable));
		}

		public static string getClassInheritanceSuffix(this BaseTemplate template, Type t)
		{
			string suffix;
			if (t.BaseType == null
				|| t.BaseType.FullName.Contains("DisplayName")
				|| t.IsEnum
				//|| t.BaseType.FullName.Contains("Secur")
					|| t.IsValueType
					|| t.BaseType == typeof(System.Delegate)
					|| t.BaseType == typeof(System.MulticastDelegate)
					|| t.BaseType.IsSealed
				)
				suffix = string.Empty;

			else if (isEnumerableType(t))//Then the base-most Type has to also be IEnumerable			
			{
				Type b = BasemostType(t);//same Type as caller's if none found.
				if (b.IsGenericType)//b.IsGenericType
				{
					Type arg0 = b.GetGenericArguments()[0];
					suffix = string.Format(": List<{0}>", arg0.Name);
				}
				else if (b.GetInterfaces().Any(iT => iT.IsGenericType))
				{
					var iType = b.GetInterfaces().First(iT => iT.IsGenericType);
					Type arg0 = iType.GetGenericArguments()[0];
					suffix = string.Format(": List<{0}>", arg0.Name);
				}
				else
					suffix = ": " + typeof(System.Collections.IEnumerable).FullName;
			}
			else if (t.BaseType != typeof(System.Object) && !ContainsExcludedSubstr(t.ToGenericTypeFullNameString(), template.ExcludeTypeFullNameSubstrings))//we CAN totally remove class inheritance (for serialization purposes); that's actually what svcutil.exe does.
			{
				suffix = ": " + ToGenericTypeFullNameString(t.BaseType);
			}
			else
				suffix = "";// 

			suffix = suffix.Trim().TrimEnd(',');
			suffix += "\r\n";
			return suffix;
		}
		public static string GetClassPrefix(Type t)
		{
			string classPrefix = "public ";

			//[CollectionDataContract]?
			if (!t.GetInterfaces().Contains(typeof(System.Collections.IEnumerable))
				&& !t.IsInterface && !t.IsValueType && !t.IsEnum)
				classPrefix = "[DataContract] " + classPrefix;

			if (t.IsInterface)
				classPrefix += "interface";
			else if (t.IsValueType && !t.IsEnum)
				classPrefix += "struct";
			else if (t.IsEnum)
				classPrefix += "enum";
			else
				classPrefix += "partial class";
			return classPrefix;
		}

		public static Dictionary<string, string> CreateMappings(IEnumerable<string> src, IEnumerable<string> dest)
		{
			if (src.Count() != dest.Count())
				throw new ArgumentException("Incorrect input: Count is not equal.");

			Dictionary<string, string> mapping = new Dictionary<string, string>();
			string[] srcArray = src.ToArray();
			string[] destArray = dest.ToArray();
			for (int i = 0; i < srcArray.Length; i++)
			{
				mapping.Add(srcArray[i], destArray[i]);
			}
			return mapping;
		}



		static Assembly mscorlib = typeof(System.String).Assembly;

		#region dead code
		//static void startExplorer(string argstring)
		//{
		//    System.Diagnostics.ProcessStartInfo info = new System.Diagnostics.ProcessStartInfo
		//    {
		//        UseShellExecute = true,
		//        FileName = "Explorer.exe",
		//        Arguments = argstring
		//    };
		//    System.Diagnostics.Process.Start(info);
		//}
		///// <summary>
		///// this is broken. do not use.
		///// </summary>
		///// <param name="t"></param>
		///// <returns></returns>
		//public static int GetObjectDepth(Type t)
		//{
		//    int depth = 0;

		//    PropertyInfo[] props = t.GetProperties();
		//    if (props.Count() == 0)
		//        return 0;
		//    else
		//    {
		//        foreach (var p in props)
		//        {
		//            if (p.PropertyType != t)//prevent infinitely-recursive definition
		//            {
		//                int subdepth = 1 + GetObjectDepth(p.PropertyType);
		//                if (subdepth > depth)
		//                    depth = subdepth;
		//            }
		//        }
		//    }
		//    return depth;
		//}

		//public static IEnumerable<Tuple<string, string>> GetPropertiesToSerialize(this Type srcType
		//    , IEnumerable<string> fullname_substr_excludes, IEnumerable<Type> mapTypesToDynamicObject)
		//{

		//    var properties = from p in srcType.GetProperties()
		//                     let propType = IsTypeMappedToDynamicObject(p.PropertyType, mapTypesToDynamicObject) ? typeof(System.Dynamic.DynamicObject) : p.PropertyType
		//                     where p.CanRead && p.CanWrite
		//                     && p.PropertyType != null
		//                     && p.PropertyType.FullName != null
		//                    && !ContainsExcludedSubstr(p.PropertyType.FullName, fullname_substr_excludes)
		//                    && !p.PropertyType.IsInterface
		//                    && !(p.Name == "Item" && p.GetIndexParameters().Count() > 0)//exclude indexers: we're serializing data not methods.
		//                     select new Tuple<string, string>(propType.ToGenericTypeFullNameString(), p.Name);
		//    return properties;

		//}
		#endregion


	}
}
